"""
执行器
"""
import ast
import json
import os
import socket
import time
import traceback
import re
import uuid

import xlwt
from DrissionPage._pages.chromium_page import ChromiumPage
from DrissionPage._units.actions import Actions
from PyQt5.QtCore import pyqtSignal


# 定义一个安全的eval函数
def safe_eval(expression, globals=None, locals=None):
    if globals is None:
        globals = {}
    if locals is None:
        locals = {}
    # 只允许使用白名单内的函数
    allowed_functions = {'len', 'sum', 'abs', 'min', 'max', 'list', 'dict', 'int', 'float'}
    # 检查表达式中是否使用了不允许的函数
    for node in ast.walk(ast.parse(expression, mode='eval')):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
            name = node.func.id
            if name not in allowed_functions:
                raise SyntaxError('Only allowed functions can be used: {}'.format(', '.join(allowed_functions)))
    # 使用globals和locals参数安全执行eval
    return eval(expression, globals, locals)


def write_excel(li, name):
    f = xlwt.Workbook()
    sheet1 = f.add_sheet('sheet', cell_overwrite_ok=True)
    index = 0
    for i in li:
        lie = 0
        for j in i:
            sheet1.write(index, lie, j)
            lie = lie + 1
        index = index + 1
    f.save(name)


class FlowBus:
    """
    流程总线
    """

    def __init__(self, flow_list: list[dict],
                 message_signal: pyqtSignal,
                 status_signal: pyqtSignal,
                 chrome_page: ChromiumPage, parent=None):
        self.flows = flow_list
        # 执行步骤索引
        self.step_index = 0
        # 退回点栈
        self.back_point = []
        # 计次循环次数栈
        self.loop_count = []
        # 输出日志到界面的信号量
        self.message_signal = message_signal
        # 状态信号量
        self.status_signal = status_signal
        # dp浏览器对象
        self.chrome_page = chrome_page
        # 全局共用一个 action
        self.action = Actions(self.chrome_page)
        # 父对象
        self.parent = parent
        # 临时变量存储，用于置变量
        self.temp_var = {}
        # excel数据
        self.excel_data = []

    def run(self):
        """
        执行流程
        """
        while self.step_index < len(self.flows):
            if self.parent and not self.parent.is_running:
                break
            # 获取当前步骤
            flow = self.flows[self.step_index]
            # 执行步骤
            flow_name = flow.get('flow_name')
            # 获取参数
            params = flow.get('params')
            flow_start_delay = flow.get('flow_start_delay', 0)
            flow_end_delay = flow.get('flow_end_delay', 0)
            if str(flow_start_delay) != '0':
                self.message_signal.emit('流程组件开始等待时间 =======> ' + str(flow_start_delay) + ' 秒', 'flow')
                time.sleep(int(flow_start_delay))
            if flow_name == '无限循环首':
                self.message_signal.emit('执行 =======> 无限循环首', 'flow')
                self.back_point.append(self.step_index)
            elif flow_name == '退出无限循环':
                self.message_signal.emit('执行 =======> 退出无限循环', 'flow')
                # 向后搜索无限循环尾,并且将无限循环首的下一个步骤作为下一个执行步骤
                try:
                    self.step_index = next(
                        i for i, flow_ in enumerate(self.flows[self.step_index:], start=self.step_index) if
                        flow_.get('flow_name') == '无限循环尾')
                except StopIteration:
                    raise Exception('未找到【无限循环尾】,请检查流程配置')
            elif flow_name == '无限循环尾':
                self.message_signal.emit('执行 =======> 无限循环尾', 'flow')
                self.step_index = self.back_point.pop()
                continue
            elif flow_name == '计次循环首':
                if (not (self.loop_count and self.loop_count[-1]) or
                        (self.loop_count and self.loop_count[-1] and self.loop_count[-1][2] != self.step_index)):
                    # 说明是首次执行计次循环首,将计次总循环次数和当前执行索引丢入栈
                    self.loop_count.append([0, int(params['count']), self.step_index])
                # 设置返回点
                self.back_point.append(self.step_index)
                self.message_signal.emit(
                    '执行 =======> 计次循环首,总循环次数:' + str(params['count']) + '当前循环次数:' + str(
                        self.loop_count[-1][0] + 1) + ' 当前执行步骤索引:' + str(
                        self.flows[self.step_index].get('index')), 'flow')
                if int(params['count']) == 0:
                    # 设置了循环次数为0的情况,向下搜索计次循环尾，跳过中间所有的步骤
                    try:
                        self.step_index = next(
                            i for i, flow_ in enumerate(self.flows[self.step_index:], start=self.step_index) if
                            flow_.get('flow_name') == '计次循环尾')
                    except StopIteration:
                        raise Exception('未找到【计次循环尾】,请检查流程配置')
            elif flow_name == '计次循环尾':
                self.message_signal.emit('执行 =======> 计次循环尾', 'flow')
                # 计次循环次数加一,并且将计次总循环次数和当前执行索引丢入栈
                count = self.loop_count.pop()
                count = [count[0] + 1, count[1], count[2]]
                if count[0] < count[1]:
                    # 判断循环次数是否超过,如果没有超过,则将计次总循环次数和当前执行索引丢入栈
                    self.loop_count.append(count)
                    # 设置返回点
                    self.step_index = self.back_point.pop()
                    continue
                # 设置尾节点运行成功
                self.status_signal.emit(int(self.flows[self.step_index].get('index')) - 1, True)
                # 设置循环首执行成功
                self.status_signal.emit(int(self.flows[count[2]].get('index')) - 1, True)
            elif flow_name == '退出计次循环':
                self.message_signal.emit('执行 =======> 退出计次循环', 'flow')
                # 向后搜索计次循环尾,并且将计次循环首的下一个步骤作为下一个执行步骤
                try:
                    self.status_signal.emit(int(self.flows[self.step_index].get('index')) - 1, True)
                    self.step_index = next(
                        i for i, flow_ in enumerate(self.flows[self.step_index:], start=self.step_index) if
                        flow_.get('flow_name') == '计次循环尾')
                    # 计次循环次数推出，防止内存溢出
                    count = self.loop_count.pop()
                    # 设置尾节点运行成功
                    self.status_signal.emit(int(self.flows[self.step_index].get('index')) - 1, True)
                    # 设置首节点运行成功
                    self.status_signal.emit(int(self.flows[count[2]].get('index')) - 1, True)
                except StopIteration:
                    raise Exception('未找到【计次循环尾】,请检查流程配置')
            elif flow_name == '元素出现首':
                selector = params.get('selector')
                timeout = params.get('timeout', 0.5)
                if not selector:
                    raise Exception('判断元素出现的selector不能为空')
                if not timeout:
                    raise Exception('判断元素出现的超时时间不能为空')
                timeout = float(timeout)
                ele = self.find_ele(selector, timeout=timeout)
                try:
                    if not ele:
                        # 如果元素不存在，则向后搜索元素出现尾，并且将元素出现首的下一个步骤作为下一个执行步骤
                        self.step_index = next(
                            i for i, flow_ in enumerate(self.flows[self.step_index:], start=self.step_index) if
                            flow_.get('flow_name') == '元素出现尾')
                except StopIteration:
                    raise Exception('未找到【元素出现尾】,请检查流程配置')
            elif flow_name == '元素未出现首':
                self.message_signal.emit(
                    '执行 =======> 元素未出现首 执行参数:' + json.dumps(params, ensure_ascii=False), 'flow')
                selector = params.get('selector')
                timeout = params.get('timeout', 0.5)
                if not selector:
                    raise Exception('判断元素未出现的selector不能为空')
                if not timeout:
                    raise Exception('判断元素未出现的超时时间不能为空')
                timeout = float(timeout)
                ele = self.find_ele(selector, timeout=timeout)
                try:
                    if ele:
                        # 如果元素存在，则向后搜索元素出现尾，并且将元素出现首的下一个步骤作为下一个执行步骤
                        self.step_index = next(
                            i for i, flow_ in enumerate(self.flows[self.step_index:], start=self.step_index) if
                            flow_.get('flow_name') == '元素未出现尾')
                except StopIteration:
                    raise Exception('未找到【元素未出现尾】,请检查流程配置')
            else:
                flow_item = self.flows[self.step_index]
                index = flow_item.get('index')
                try:
                    self.execute(flow_name, params)
                    # 上报执行状态
                    self.status_signal.emit(int(index) - 1, True)
                except Exception as e:
                    err_str = f'第{flow_item.get("index")}行,执行流程组件【{flow_name}】出错:{str(e)}'
                    self.message_signal.emit(err_str, 'error')
                    # 上报执行状态
                    self.status_signal.emit(int(index) - 1, False)
                    traceback.print_exc()
                    break
            self.step_index += 1
            # print('流程组件结束等待时间 =======>', flow_end_delay, '秒')
            if str(flow_end_delay) != '0':
                self.message_signal.emit('流程组件结束等待时间 =======> ' + str(flow_end_delay) + ' 秒', 'flow')
                time.sleep(int(flow_end_delay))
            time.sleep(int(flow_end_delay))
        # self.parent.signals.stop_signal.emit()

    def print_log(self, params):
        """
        打印日志
        :param params: 参数
        """
        # 取年月日时分秒
        now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
        self.message_signal.emit(now + ' : ' + params.get('message', ''), 'info')

    @staticmethod
    def wait_by_second(params):
        """
        按秒等待
        :param params:
        :return:
        """
        time.sleep(float(params.get('time', 0)))

    def get_url(self, params):
        """
        访问网页
        :param params:
        :return:
        """
        url = params.get('url')
        retry = params.get('retry', 3)
        if not url:
            raise Exception('打开页面的url不能为空')
        self.chrome_page.get(url=url, retry=retry)

    def ele_get_screenshot(self, params):
        """
        元素截图
        :param params:
        :return:
        """
        selector = params.get('selector')
        save_path = params.get('save_path')
        if not selector or not save_path:
            raise Exception('截图元素的selector或者存储路径不能为空')
        # 检测save_path是否存在，不存在则创建
        if not os.path.exists(save_path):
            os.makedirs(save_path)
        name_type = params.get('name_type', '格式化时间')
        if name_type == '格式化时间':
            filename = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
        elif name_type == '时间戳':
            filename = str(int(time.time() * 1000))
        elif name_type == 'UUID':
            filename = str(uuid.uuid1()).replace('-', '')
        else:
            # 默认时间戳
            filename = str(int(time.time() * 1000))
        ele = self.chrome_page.ele(selector)
        if not ele:
            raise Exception('需要截图的元素未找到')
        ele.get_screenshot(path=save_path, name=filename)

    def ele_get_screenshot_and_tcp_upload(self, params):
        """
        元素截图并进行tcp上报
        :param params:
        :return:
        """
        selector = params.get('selector')
        upload_address = params.get('upload_address')
        if not selector or not upload_address:
            raise Exception('截图元素的selector或者上报IP:PORT不能为空')
        # 检测upload_address是否正确，格式为IP:PORT
        if ':' not in upload_address:
            raise Exception('上报地址格式错误')
        ip, port = upload_address.split(':')
        ele = self.chrome_page.ele(selector)
        if not ele:
            raise Exception('需要截图的元素未找到')
        pic_bytes = ele.get_screenshot(as_bytes=True)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((ip, int(port)))
        # 发送图片大小
        sock.sendall(len(pic_bytes).to_bytes(8, 'big'))
        # 发送图片数据
        sock.sendall(pic_bytes)
        print("Image sent successfully.")
        sock.close()

    def ele_click(self, params):
        """
        元素点击
        :param params:
        :return:
        """
        # 点击元素
        selector = params.get('selector')
        x = params.get('x', 0)
        y = params.get('y', 0)
        if not selector:
            raise Exception('点击元素的selector不能为空')
        ele = self.chrome_page.ele(selector)
        if not ele:
            raise Exception('需要点击的元素未找到')
        self.action.move_to(ele, offset_x=x, offset_y=y)
        time.sleep(1)
        self.action.click(selector)

    def ele_input(self, params):
        """
        元素内容输入
        :param params: 参数
        :return:
        """
        # 输入元素
        selector = params.get('selector')
        text = params.get('content')
        if not selector:
            raise Exception('输入元素的selector不能为空')
        if not text:
            raise Exception('输入元素的text不能为空')
        ele = self.chrome_page.ele(selector)
        if not ele:
            raise Exception('需要输入内容的元素未找到')
        ele.input(text, clear=True)

    def wait_until_tagging(self, params):
        """
        等待，直到标注完成
        :return:
        """
        port = params.get('port', 32222)
        success_str = params.get('callback_str', 'ok')
        if not success_str:
            raise Exception('等待标注完成的回调字符串不能为空')
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('0.0.0.0', int(port)))
        sock.listen(1)
        print("Waiting for connection...")
        conn, addr = sock.accept()
        print(f"Connected by {addr}")

        # 接收文本的大小为success_str的长度
        data_size = int.from_bytes(conn.recv(8), 'big')
        # 接收文本数据
        received_data = b""
        while True:
            while len(received_data) < data_size:
                packet = conn.recv(4096)
                if not packet:
                    break
                received_data += packet
            # received_data转文本
            received_data = received_data.decode('utf-8')
            if received_data == success_str:
                break
            received_data = b""
        conn.close()
        sock.close()

    def replace_cmd_params(self, param_value):
        """
        替换指令中的参数
        :param param_value: 参数值
        :return:
        """
        wait_deal = param_value
        match_list = re.findall(r'\{(.*?)}', wait_deal)
        for match in match_list:
            if self.loop_count:
                match_replace_result = match.replace('取当前循环次数()', str(self.loop_count[-1][0] + 1))
                result = safe_eval(match_replace_result)
                match = '{' + match + '}'
                wait_deal = wait_deal.replace(match, str(result))
            # 再处理temp_var中的值
            if match in self.temp_var:
                wait_deal = wait_deal.replace('{' + match + '}', str(self.temp_var.get(match, '')))

        return wait_deal

    def execute(self, flow_name, params):
        """
        执行方法
        :param flow_name:
        :param params:
        :return:
        """
        # print('执行方法:', flow_name, '参数:', params)
        self.message_signal.emit(f'执行 =======> {flow_name}' + ' 参数:' + json.dumps(params, ensure_ascii=False),
                                 'flow')
        col_list = ['selector', 'message', 'url', 'excel_save_path', 'excel_col_content', 'head_list']
        origin_params = json.loads(json.dumps(params, ensure_ascii=False))
        for col_name in col_list:
            if params.get(col_name):
                # 替换相关指令
                params[col_name] = self.replace_cmd_params(params.get(col_name))
        if flow_name == '打印日志':
            self.print_log(params)
        elif flow_name == '等待':
            self.wait_by_second(params)
        elif flow_name == '访问网页':
            self.get_url(params)
        elif flow_name == '停止页面加载':
            self.chrome_page.run_cdp('Page.stopLoading')
        elif flow_name == '元素截图':
            self.ele_get_screenshot(params)
        elif flow_name == '鼠标向左移动':
            distance = params.get('distance', 100)
            # 按住元素向左移动
            self.action.left(distance)
        elif flow_name == '鼠标向右移动':
            distance = params.get('distance', 100)
            # 按住元素向右移动
            self.action.right(distance)
        elif flow_name == '鼠标按住':
            # 按住元素
            selector = params.get('selector')
            if not selector:
                raise Exception('按住元素的selector不能为空')
            ele = self.chrome_page.ele(selector)
            if not ele:
                raise Exception('需要按住的元素未找到')
            self.action.hold(ele)
        elif flow_name == '鼠标释放':
            # 释放元素
            self.action.release()
        elif flow_name == '元素点击':
            self.ele_click(params)
        elif flow_name == '输入内容':
            self.ele_input(params)
        elif flow_name == '元素截图上报':
            self.ele_get_screenshot_and_tcp_upload(params)
        elif flow_name == '等待标注完成':
            self.wait_until_tagging(params)
        elif flow_name == '取元素属性':
            attribute = self.get_element_attribute(params)
            self.temp_var[params.get('var_name')] = attribute
        elif flow_name == '取元素内纯文本':
            text = self.get_element_text(params)
            self.temp_var[params.get('var_name')] = text
        elif flow_name == '插入一行':
            self.excel_data.append([])
        elif flow_name == '插入多列':
            col_content = params.get('excel_col_content')
            if not col_content:
                raise Exception('插入多列的内容不能为空')
            if len(self.excel_data) == 0:
                self.excel_data.append([])
            col_list = col_content.split(',')
            for col_content in col_list:
                self.excel_data[-1].append(col_content)
        elif flow_name == '导出Excel':
            head_list = params.get('head_list')
            excel_save_path = params.get('excel_save_path')
            if not head_list:
                raise Exception('导出Excel的头不能为空')
            if not excel_save_path:
                raise Exception('导出Excel的保存路径不能为空')
            head_list = head_list.split(',')
            self.excel_data.insert(0, head_list)
            # 判断excel_save_path后缀是否为xls
            if not excel_save_path.endswith('.xls'):
                excel_save_path = excel_save_path + '.xls'
            # 将head_list插入到第一行
            write_excel(self.excel_data, excel_save_path)
        # 执行完所有方法以后，复原模板
        for key_name in origin_params:
            params[key_name] = origin_params[key_name]

    def get_element_text(self, params):
        """
        获取元素内纯文本
        :param params:
        :return:
        """
        selector = params.get('selector')
        if not selector:
            raise Exception('获取元素内纯文本的selector不能为空')
        ele = self.chrome_page.ele(selector)
        if not ele:
            raise Exception('需要获取文本的元素未找到')
        return ele.text

    def find_ele(self, selector, timeout=None):
        """
        查找元素，如果元素存在返回True，否则返回False
        :return:  bool
        """
        ele = self.chrome_page.ele(selector, timeout=timeout)
        if not ele:
            return False
        else:
            return True

    def get_element_attribute(self, params):
        """
        获取元素属性
        :param params:
        :return:
        """
        selector = params.get('selector')
        attribute = params.get('attribute')
        if not selector:
            raise Exception('获取元素属性的selector不能为空')
        if not attribute:
            raise Exception('获取元素属性的attribute不能为空')
        ele = self.chrome_page.ele(selector)
        if not ele:
            raise Exception('需要获取属性的元素未找到')
        return ele.attr(attribute)

    def get_latest_insert_variables_flow(self):
        """
        获取最新插入变量的流程
        :return:
        """
        # 从当前执行步骤往前搜索，找到最近的置变量首
        for i in range(self.step_index - 1, -1, -1):
            if self.flows[i].get('flow_name') == '置变量首':
                return self.flows[i]
        return None


if __name__ == '__main__':
    #     flow = [
    #         {
    #             "flow_name": '无限循环首',
    #             "params": {},
    #             'flow_start_delay': 0,
    #             'flow_end_delay': 0
    #         }, {
    #             "flow_name": '打开页面',
    #             "params": {}
    #         }, {
    #             "flow_name": '停止页面加载',
    #             "params": {}
    #         }, {
    #             "flow_name": '退出无限循环',
    #             "params": {}
    #         }, {
    #             "flow_name": '无限循环尾',
    #             "params": {}
    #         }, {
    #             "flow_name": '停止页面加载',
    #             "params": {}
    #         }, {
    #             "flow_name": '计次循环首',
    #             "params": {
    #                 "count": 3
    #             }
    #         }, {
    #             "flow_name": '打开页面',
    #             "params": {}
    #         }, {
    #             "flow_name": '停止页面加载',
    #             "params": {}
    #         }, {
    #             "flow_name": '退出计次循环',
    #             "params": {}
    #         }, {
    #             "flow_name": '计次循环尾',
    #             "params": {}
    #         }
    #     ]
    #     bus = FlowBus(flow)
    #     bus.run()
    # # [无限循环首,打开页面,停止页面加载,退出无限循环,无限循环尾,停止页面加载]
    # # stack = {},step_index = 0
    # # 无限循环首丢入栈
    # # 栈顶元素执行
    # # 如果是无限循环首，不管
    # # 如果是其他指令，执行
    # # 如果是退出无限循环，栈顶元素出栈
    # 使用示例
    # expression = "sum(list(map(int, ['1', '2', '3'])))"

    selector = 'x://div[@class="login-pwd-wp"]//form/div[{取当前循环次数()+1}]/input'
    match_list = re.findall(r'\{(.*?)}', selector)
    for match in match_list:
        print(match)
        match_replace_result = match.replace('取当前循环次数()', str(1))
        result = safe_eval(match_replace_result)
        match = '{' + match + '}'
        selector = selector.replace(match, str(result))
    print(selector)
    # selector = selector.replace('取当前循环次数()', str(self.loop_count[-1][0] + 1))
    # expression = "1+2"
    # result = safe_eval(expression)
    # print(result)  # 输出: 6
